Categoría: Seguridad

  • Seguridad de sesiones y cookies en PHP: guía práctica

    Seguridad de sesiones y cookies en PHP: guía práctica

    Resumen: Esta guía práctica resume las medidas esenciales para gestionar sesiones y cookies en PHP de forma segura: inicio de sesión, regeneración de ID, cookies seguras, timeouts, almacenamiento eficiente y limpieza al cerrar sesión.

    Introducción

    Sesiones y cookies permiten mantener estado en aplicaciones web PHP: autenticación, preferencias y datos temporales. Usadas correctamente mejoran la experiencia; mal configuradas, introducen riesgos de seguridad.

    Prerrequisitos

    Antes de aplicar las prácticas descritas asegúrate de servir las páginas sensibles por HTTPS y de tener control sobre el código que inicia y destruye sesiones. No afirmaré compatibilidades concretas; valida esto según tu entorno.

    Conocimiento mínimo necesario: saber dónde llamar a session_start() y cómo enviar cookies desde PHP. Evita exponer identificadores de sesión en URLs o registros.

    Desarrollo

    Procedimiento

    Pasos prácticos y concisos para asegurar sesiones y cookies en PHP. Aplique cada paso según el riesgo y la arquitectura de su aplicación.

    1. Iniciar sesiones de forma controlada y consistente.
    2. Regenerar el identificador de sesión tras la autenticación.
    3. Enviar cookies con flags Secure, HttpOnly y SameSite cuando corresponda.
    4. Implementar timeouts por inactividad y limpieza al logout.
    5. Almacenar solo identificadores en la sesión; datos pesados en base de datos o cache.
    6. Opcional: usar un session handler personalizado para almacenamiento centralizado.

    Detalles clave: regenerar el ID reduce el riesgo de session fixation; Secure y HttpOnly protegen el cookie en tránsito y frente a JavaScript; SameSite mitiga ciertas CSRF.

    Pro tip: Usa sesiones para identificar entidades (IDs) y no para persistir grandes objetos o datos sensibles sin cifrado.

    Ejemplos

    A continuación se incluyen ejemplos prácticos adaptados desde patrones comunes. Escapa los marcadores de apertura PHP en tu código según la plantilla del bloque.

    <?php
    // Start the session
    session_start();
    
    // Store some data in the session
    $_SESSION['username'] = 'JohnDoe';
    
    // Retrieve data from the session
    echo 'Hello, ' . $_SESSION['username']; // Outputs: Hello, JohnDoe
    Lenguaje del código: PHP (php)

    Regenerar el ID de sesión inmediatamente después de un login exitoso:

    <?php
    session_start();
    
    // After a successful login
    if ($_POST['username'] == 'user' && $_POST['password'] == 'password') {
        session_regenerate_id(true);  // Regenerate the session ID for added security
        
        $_SESSION['username'] = 'user';
        echo "Login successful, and your session ID has been regenerated!";
    }
    Lenguaje del código: PHP (php)

    Ejemplo de cookie segura con atributos recomendados (transmisión segura y protección contra acceso por JS):

    <?php
    // Set a cookie that only works on secure connections (HTTPS)
    setcookie('user_preference', 'dark_mode', [
        'expires' => time() + 3600, // 1 hour expiration
        'path' => '/',
        'domain' => 'yourdomain.com',
        'secure' => true,  // Send cookie only over HTTPS
        'httponly' => true,  // Prevent JavaScript access to cookie
        'samesite' => 'Strict' // Helps prevent CSRF attacks
    ]);
    
    echo 'Your preference cookie is set securely.';
    Lenguaje del código: PHP (php)

    Timeout de sesión por inactividad (mecanismo simple basado en marcas temporales):

    <?php
    session_start();
    
    // Set timeout period (10 minutes)
    $timeout = 600;
    
    if (isset($_SESSION['LAST_ACTIVITY']) && (time() - $_SESSION['LAST_ACTIVITY'] > $timeout)) {
        // If the session has expired, destroy it
        session_unset();
        session_destroy();
        echo 'Session expired. Please log in again.';
    } else {
        $_SESSION['LAST_ACTIVITY'] = time(); // Update last activity time
        echo 'Your session is still active.';
    }
    Lenguaje del código: PHP (php)

    Esqueleto de un session handler personalizado para almacenar sesiones en base de datos o cache centralizada:

    <?php
    class CustomSessionHandler extends SessionHandler {
        public function read($session_id) {
            // Read session data from the database
        }
    
        public function write($session_id, $data) {
            // Write session data to the database
        }
    }
    
    // Register custom session handler
    $handler = new CustomSessionHandler();
    session_set_save_handler($handler, true);
    session_start();
    Lenguaje del código: PHP (php)

    Al cerrar sesión, destruye la sesión y expira las cookies relevantes para evitar reutilización:

    <?php
    session_start();
    
    // Destroy the session and clear all session data
    session_unset();
    session_destroy();
    
    // Expire session and preference cookies
    setcookie(session_name(), '', time() - 3600, '/');
    setcookie('user_preference', '', time() - 3600, '/');
    
    echo 'Session and cookies have been cleared.';
    Lenguaje del código: PHP (php)

    Checklist

    1. Servir páginas sensibles por HTTPS.
    2. Iniciar sesión con session_start() solo donde sea necesario.
    3. Regenerar ID de sesión tras autenticación (session_regenerate_id).
    4. Enviar cookies con Secure, HttpOnly y SameSite adecuados.
    5. Implementar timeout por inactividad y actualizar LAST_ACTIVITY.
    6. Almacenar solo identificadores en la sesión; consultar DB para datos grandes.
    7. Destruir sesión y expirar cookies en logout.

    Conclusión

    La seguridad de sesiones y cookies es una combinación de buenas prácticas: transporte seguro (HTTPS), configuración correcta de cookies, rotación de identificadores, timeouts y limpieza al logout. Estas medidas reducen riesgos comunes como hijacking y XSS.

    Empieza aplicando los pasos del checklist y adapta las implementaciones (por ejemplo, handlers personalizados) según la escala y arquitectura de tu aplicación.

  • Implementar TOTP (MFA) en Symfony 7: guía práctica

    Implementar TOTP (MFA) en Symfony 7: guía práctica

    \n

    En esta guía práctica se explica cómo añadir autenticación multifactor TOTP (Time-based One-Time Password) a una aplicación Symfony 7. Incluye el servicio TOTP, cambios en la entidad, migración, controladores, formulario, suscriptor de eventos y recomendaciones de seguridad.

    \n\n\n\n\n\n\n\n

    Introducción

    \n\n\n\n

    TOTP es el estándar (RFC 6238) que usan Google Authenticator, Authy y similares. Genera códigos de un solo uso a partir de un secreto compartido y la hora actual.

    \n\n\n\n

    Esta implementación está pensada para integrarse con el sistema de autenticación de Symfony: genera secretos, muestra un QR para el usuario, verifica códigos en el login y obliga a verificar tras el login usando la sesión.

    \n\n\n\n

    Prerrequisitos

    \n\n\n\n
    • Proyecto Symfony 7 con sistema de autenticación ya configurado.
    • Doctrine ORM (para persistir el secreto).
    • Conocimientos básicos de seguridad en Symfony.
    \n\n\n\n

    Instalar la dependencia para generar QR (se usa Endroid en el ejemplo):

    \n\n\n\n
    composer require endroid/qr-code
    \n\n\n\n

    Desarrollo

    \n\n\n\n

    Procedimiento

    \n\n\n\n

    Resumen de pasos implementados en el proyecto:

    \n\n\n\n
    1. Agregar campos TOTP a la entidad User y crear migración.
    2. Crear un servicio TotpService con generación/verificación de códigos y generación del URI otpauth://.
    3. Crear formularios y controladores para habilitar, verificar y deshabilitar 2FA.
    4. Registrar un EventSubscriber que intercepte peticiones y redirija a la verificación cuando corresponda.
    \n\n\n\n

    A continuación se muestran fragmentos clave (no es necesario copiar todo el archivo, adapte según su proyecto).

    \n\n\n\n

    Entidad: añadir campos para estado y secreto TOTP.

    \n\n\n\n
    <?php\n// src/Entity/User.php (fragmento)\n\n#[ORM\Column(name: 'totp_enabled', type: 'boolean', options: ['default' => false])]\nprivate bool $totpEnabled = false;\n\n#[ORM\Column(name: 'totp_secret', type: 'string', length: 64, nullable: true)]\nprivate ?string $totpSecret = null;\n\npublic function isTotpEnabled(): bool\n{\n    return $this->totpEnabled;\n}\n\npublic function setTotpEnabled(bool $enabled): self\n{\n    $this->totpEnabled = $enabled;\n    return $this;\n}\n\npublic function getTotpSecret(): ?string\n{\n    return $this->totpSecret;\n}\n\npublic function setTotpSecret(?string $secret): self\n{\n    $this->totpSecret = $secret ? trim($secret) : null;\n    return $this;\n}\n
    \n\n\n\n

    Migración: ejemplo que añade las columnas totp_enabled y totp_secret.

    \n\n\n\n
    <?php\n// migrations/Version20240101000000.php (fragmento)\n\npublic function up(Schema $schema): void\n{\n    $table = $schema->getTable('users');\n    if (!$table->hasColumn('totp_enabled')) {\n        $table->addColumn('totp_enabled', 'boolean', ['default' => false]);\n    }\n    if (!$table->hasColumn('totp_secret')) {\n        $table->addColumn('totp_secret', 'string', ['length' => 64, 'notnull' => false]);\n    }\n}\n
    \n\n\n\n

    Servicio TotpService: responsabilidades principales — generar secreto, generar/verificar códigos y crear URI de aprovisionamiento.

    \n\n\n\n
    <?php\n// src/Service/TotpService.php (fragmento)\n\npublic function generateSecret(int $length = 32): string\n{\n    $bytes = random_bytes($length);\n    return rtrim(strtr(base64_encode($bytes), '+/', 'XY'), '=');\n}\n\npublic function getProvisioningUri(string $label, string $issuer, string $secret): string\n{\n    $label = rawurlencode($label);\n    $issuerEncoded = rawurlencode($issuer);\n\n    return sprintf(\n        'otpauth://totp/%s?secret=%s&issuer=%s&period=%d&digits=%d&algorithm=%s',\n        $label,\n        $secret,\n        $issuerEncoded,\n        $this->period,\n        $this->digits,\n        strtoupper($this->algorithm)\n    );\n}\n\npublic function verifyCode(string $secret, string $code, int $window = 1): bool\n{\n    $now = time();\n    $code = trim($code);\n    for ($i = -$window; $i <= $window; $i++) {\n        $timestamp = $now + ($i * $this->period);\n        if (hash_equals($this->getCode($secret, $timestamp), $code)) {\n            return true;\n        }\n    }\n    return false;\n}\n
    \n\n\n\n

    Form types: formularios sencillos para habilitar y deshabilitar 2FA que aceptan el código de 6 dígitos.

    \n\n\n\n
    <?php\n// src/Form/User/TwoFactorEnableType.php (fragmento)\n\n$builder->add('code', TextType::class, [\n    'label' => 'Authentication code',\n    'attr' => [\n        'autocomplete' => 'one-time-code',\n        'inputmode' => 'numeric',\n        'pattern' => '[0-9]*',\n        'maxlength' => 6,\n    ],\n]);\n
    \n\n\n\n

    Controlador de ajustes: genera secreto temporal en sesión, muestra QR y confirma el código antes de persistir el secreto en la base de datos.

    \n\n\n\n
    <?php\n// src/Controller/User/SettingsController.php (fragmento)\n\n$pendingSecret = $session->get('2fa_pending_secret');\n// Regenerar si no existe o expiró\nif (!$pendingSecret || (time() - $session->get('2fa_pending_secret_time', 0)) > 600) {\n    $pendingSecret = $totpService->generateSecret();\n    $session->set('2fa_pending_secret', $pendingSecret);\n    $session->set('2fa_pending_secret_time', time());\n}\n\n$uri = $totpService->getProvisioningUri($user->getEmail(), $request->getHttpHost(), $pendingSecret);\n$qrSvg = $totpService->generateInlineSvgQr($uri, 180);\n
    \n\n\n\n

    Suscriptor de eventos: intercepta solicitudes y redirige a la ruta de verificación si el usuario tiene 2FA activado pero no ha verificado la sesión.

    \n\n\n\n
    <?php\n// src/EventSubscriber/UserTwoFactorSubscriber.php (fragmento)\n\nif (!$event->isMainRequest()) {\n    return;\n}\n\n$request = $event->getRequest();\nif (!str_starts_with($request->getPathInfo(), '/panel')) {\n    return; // limitar al panel de usuario\n}\n\n$user = $this->getAuthenticatedUser();\nif (!$user || !$user->isTotpEnabled() || !$user->getTotpSecret()) {\n    return;\n}\n\n$session = $this->getSession();\nif ($session && $session->get(self::SESSION_KEY) === true) {\n    return; // ya verificado\n}\n\n$event->setResponse(new RedirectResponse($this->urlGenerator->generate('user_2fa')));\n
    \n\n\n\n

    Seguridad en la configuración del firewall: redirigir al usuario a la ruta /panel/2fa tras el login es una opción sencilla para iniciar el flujo de verificación.

    \n\n\n\n
    security:\n    firewalls:\n        user:\n            pattern: ^/panel\n            provider: user_provider\n            lazy: true\n            form_login:\n                login_path: /panel/\n                check_path: /panel/\n                enable_csrf: true\n                default_target_path: /panel/2fa\n            logout:\n                path: /panel/logout\n                target: /panel/\n\n    access_control:\n        - { path: ^/panel/?$, roles: PUBLIC_ACCESS }\n        - { path: ^/panel, roles: IS_AUTHENTICATED_REMEMBERED }\n
    \n\n\n\n

    Protecciones adicionales sugeridas: limitación de intentos (rate limiting), comparación en tiempo constante (hash_equals) y manejo cuidadoso de sesiones y secretos temporales.

    \n\n\n\n
    <?php\n// Ejemplo de rate limiting dentro del controlador de verificación\n$limiter = $twoFactorLimiter->create($request->getClientIp());\nif (!$limiter->consume()->isAccepted()) {\n    throw new TooManyRequestsHttpException(null, 'Too many attempts. Please wait.');\n}\n
    \n\n\n\n

    Ejemplos

    \n\n\n\n

    Generar un URI de aprovisionamiento (otpauth) que puedan escanear las apps autenticadoras:

    \n\n\n\n
    // Uso en controlador\n$label = $user->getEmail();\n$issuer = $request->getHttpHost();\n$secret = $totpService->generateSecret();\n$uri = $totpService->getProvisioningUri($label, $issuer, $secret);\n$qrSvg = $totpService->generateInlineSvgQr($uri, 180);\n
    \n\n\n\n

    Verificar un código recibido del usuario (ventana de tolerancia = 1 período por defecto):

    \n\n\n\n
    // Verificación\nif ($totpService->verifyCode($storedSecret, $submittedCode)) {\n    // Aceptado: marcar sesión como verificada\n    $session->set('2fa_verified', true);\n} else {\n    // Código inválido\n}\n
    \n\n\n\n

    Checklist

    \n\n\n\n
    1. Agregar totp_enabled y totp_secret en la entidad y migrar.
    2. Registrar TotpService en el contenedor y probar generación/verificación localmente.
    3. Crear formularios y controladores para setup/disable/verify.
    4. Implementar EventSubscriber para forzar verificación tras el login.
    5. Configurar rate limiting y proteger rutas sensibles.
    6. Planificar recuperación: backup codes o proceso de soporte.
    \n\n\n\n

    Conclusión

    \n\n\n\n

    Implementar TOTP sin dependencias externas ofrece control y entendimiento del flujo de MFA. El patrón mostrado (servicio TOTP + suscriptor de eventos + verificación en sesión) es ligero y fácil de adaptar a requisitos adicionales como backup codes o WebAuthn.

    \n\n\n\n

    Priorice siempre: comparación en tiempo constante, limitación de intentos y proteger los secretos en reposo. Después de implementar, pruebe el flujo completo en entornos de staging antes de desplegar en producción.

    \n\n